Skip to content

feat(stdlib): Canvas.affine — HTML5 Canvas 2D rendering (bindings #8)#510

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/canvas2d-binding
May 31, 2026
Merged

feat(stdlib): Canvas.affine — HTML5 Canvas 2D rendering (bindings #8)#510
hyperpolymath merged 1 commit into
mainfrom
claude/canvas2d-binding

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

Ships the Canvas 2D half of Tier-1 #8 of #446. This is the surface idaptik-ums (App.res: 1178 LoC of DOM + canvas) and every non-Pixi UI in the estate has been waiting for; WebGL / WebGL2 / WebGPU stays at until a consumer surfaces a concrete need.

What lands

stdlib/Canvas.affine (+170 lines, new module): 1 extern type (Ctx2D) + 26 extern fns covering the full idaptik-ums-relevant Canvas 2D surface:

Surface Externs
Context acquisition canvasGetContext2D
Styles canvasFillStyle / StrokeStyle / LineWidth / GlobalAlpha
Rectangles canvasFillRect / StrokeRect / ClearRect
Paths canvasBeginPath / ClosePath / MoveTo / LineTo / Arc / Fill / Stroke
Transform stack canvasSave / Restore / Translate / Rotate / Scale
Text canvasFont / TextAlign / TextBaseline / FillText / StrokeText / MeasureText
Images canvasDrawImage / canvasDrawImageScaled

lib/codegen_deno.ml (+58 lines): 26 __as_canvas* prelude helpers + 26 dispatch entries adjacent to the Ipc block.

tests/codegen-deno/canvas_smoke.{affine,harness.mjs} (+160 lines combined): 5 distinct smoke functions exercise every shipped extern. The MockCtx2D records every method call as a typed-op tuple so the test asserts call order + arguments, not just side-effects. The canvasMeasureText round-trip exercises the Json-return shape (the TextMetrics-shaped object).

docs/bindings-roadmap.adoc row #8 status promoted ○ → ◑; deferred items captured (WebGL, gradients/patterns, ImageData, curve primitives, compositing/clip).

Design notes

Why is HTMLCanvasElement not its own extern type? The canvas-creation entry point is host-dependent — browser document.createElement("canvas") vs idaptik's pre-existing DOM tree vs jsdom-under-Deno. Treating the canvas as opaque Json defers the typed wrapper to the natural follow-up once affinescript-dom lands runtime support (currently blocked on the wasm-codegen for-in / while gap, issue #255).

Why fix arc counter-clockwise to false? The 6-arg shape (ccw boolean) would force every call site to pass a literal false. The 5-arg shape covers the overwhelming majority of consumers; a typed wrapper with the ccw flag is a follow-up if a consumer surfaces the need.

Why two separate drawImage externs instead of one with optional args? AffineScript doesn't have JS-style variadic functions; making w and h Option<Float> would force every natural-size call to thread Nones. The two-extern split keeps each call site simple.

Why open-string eventMode / textAlign / textBaseline? Same rationale as the PixiJS expansion (#502) — Pixi 8's eventMode and Canvas's text-alignment values are open enumerations, and a sum-type binding would either freeze the set or require tagged-variant codegen that doesn't exist on the Deno-ESM backend yet (deferred to json.affine v0.3). Consistency with existing patterns.

Test plan

  • dune build bin/main.exe — clean (only the expected parser warnings)
  • dune runtest --force — 356 tests pass (was 354 pre-PR; +2 from new Canvas + Ipc modules' AOT smoke)
  • tools/run_codegen_deno_tests.sh — all 19 harnesses including the new canvas_smoke.harness.mjs OK
  • CI build job
  • CI tools/run_codegen_deno_tests.sh job
  • CI governance + Hypatia (6 baselines per repo CLAUDE.md may be red — pre-existing on main, not regressions)

Refs

🤖 Generated with Claude Code

New stdlib module covering the Tier-1 #8 Canvas 2D half — idaptik-ums
App.res's 1178-LoC DOM-and-canvas rendering surface has been waiting
for this. WebGL/WebGL2/WebGPU stays at `○` until a consumer surfaces a
concrete need.

stdlib/Canvas.affine (+170 lines, new module): 1 extern type + 26
extern fns covering the full idaptik-ums-relevant Canvas 2D surface.

| Surface | Externs |
|---|---|
| Context acquisition | canvasGetContext2D |
| Styles | canvasFillStyle / StrokeStyle / LineWidth / GlobalAlpha |
| Rectangles | canvasFillRect / StrokeRect / ClearRect |
| Paths | canvasBeginPath / ClosePath / MoveTo / LineTo / Arc / Fill / Stroke |
| Transform stack | canvasSave / Restore / Translate / Rotate / Scale |
| Text | canvasFont / TextAlign / TextBaseline / FillText / StrokeText / MeasureText |
| Images | canvasDrawImage / canvasDrawImageScaled |

`HTMLCanvasElement` crosses the boundary as opaque `Json` (the
canvas-creation entry point is host-dependent — browser
`document.createElement("canvas")` vs idaptik's pre-existing DOM
tree). Typed wrapper is the natural follow-up once
`affinescript-dom` lands runtime support (currently blocked on the
wasm-codegen for-in / while gap, issue #255).

lib/codegen_deno.ml (+58 lines): 26 `__as_canvas*` prelude helpers +
26 dispatch entries adjacent to the Ipc block.

tests/codegen-deno/canvas_smoke.{affine,harness.mjs} (+160 lines
combined): 5 distinct smoke functions exercise every shipped extern.
MockCtx2D records every method call as a typed-op tuple so the test
asserts call order + arguments, not just side-effects. measureText
round-trip exercises the Json-return shape.

docs/bindings-roadmap.adoc row #8 status promoted `○ → ◑`; deferred
items captured (WebGL, gradients/patterns, ImageData,
bezierCurveTo / quadraticCurveTo / ellipse, compositing / clip).

Refs #446 — Tier 1 #8.
@hyperpolymath hyperpolymath merged commit c0cd045 into main May 31, 2026
15 of 24 checks passed
@hyperpolymath hyperpolymath deleted the claude/canvas2d-binding branch May 31, 2026 11:15
@github-actions
Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 85 issues detected

Severity Count
🔴 Critical 2
🟠 High 15
🟡 Medium 68

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action ons/checkout@v6\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action land/setup-deno@v2\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in affine-vscode-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "affine-vscode-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant